<?php
/**
 *  Core Cx file.
 *  @file cxelement.php
 */

require_once 'cxss.php';

//+----------------------------------------------------------------------------+
//+    IxElement
//+----------------------------------------------------------------------------+

/**
 *  In conjunction with CxAbstractElementList::__call()
 *  makes these classes interusabe.
 */
interface IxElement
{
    /**
     *  Assigns element's $this to $var; chainable.
     *  @param $var CxElement
     *  @return CxElement
     */
    function into (& $var);
}

//+----------------------------------------------------------------------------+
//+    CxElement
//+----------------------------------------------------------------------------+

/**
 *  Wraps SimpleXMLElement adding some completeness.
 */
class CxElement extends SimpleXMLElement implements IxElement
{
    /**
     *  Create object Cx<tagname> if this class exists, 
     *  or use get_class() otherwise.
     *  @param $element DOMElement
     *  @return CxElement
     *  @todo optimize (90)
     */
    function create_from (DOMElement $element)
    {
        $class = "CxTag{$element->nodeName}";
        if (!class_exists($class, false)) $class = get_class($this);
        return simplexml_import_dom($element, $class);
    }

    /**
     *  Accesses element's underlying DOM node.
     *  @return DOMElement
     * 
     */
    function dom_node ()
    {
        return dom_import_simplexml($this);
    }

    /**
     *  Accesses underlying DOM node of $mixed.
     *  @param $mixed Node instance of DOMNode/SimpleXMLDocument
     *  @return DOMElement
     *  @todo optimize (70)
     */
    static function dom_node_of ($mixed)
    {
        if ($mixed instanceof DOMNode)
            return $mixed;
        if ($mixed instanceof SimpleXMLElement)
            return dom_import_simplexml($mixed);
        throw new ExNotApplicable(
            __function__, $mixed
        );
    }

    /**
     *  Imports $element into the owner DOM document; chainable.
     *  @param $element Node acceptable by CxElement::dom_node_of()
     *  @return CxElement
     */
    function import ($element)
    {
        return $this->create_from(
            $this->import_dom_node($this->dom_node_of($element))
        );
    }

    /**
     *  Imports DOM node and all its children into
     *  the owner DOM document recursively.
     *  @param $node DOMNode
     *  @return DOMNode
     */
    protected function import_dom_node (DOMNode $node)
    {
        $imported = $this->dom_node()->ownerDocument->importNode($node);
        if ($node->hasChildNodes()) foreach ($node->childNodes as $child) {
            $imported->appendChild($this->import_dom_node($child));
        }
        return $imported;
    }

    /**
     *  Imports $source into the owner DOM document; chainable.
     *  @param $source string valid XML source; see SimpleXMLElement
     *  @return CxElement
     */
    function create ($source)
    {
        return $this->import(new SimpleXmlElement($source));
    }

    /**
     *  Duplicate (clone) the element with all its descendans by default.
     *  @param $deep bool = true
     *  @return CxElement
     */
    function duplicate ($deep = true)
    {
        return $this->create_from(
            $this->dom_node()->cloneNode($deep)
        );
    }

    /**
     *  Assigns element's $this to $var; chainable.
     *  @param $var CxElement
     *  @return CxElement
     */
    function into (& $var)
    {
        return $var = $this;
    }

    /**
     *  Parent.
     *  @return CxElement
     */
    function progenitor ()
    {
        return $this->create_from($this->dom_node()->parentNode);
    }

    /**
     *  Returns IxElementIterator for all the items (CxElementList by default) to keep chaining.
     *  @param $list IxElementIterator = null
     *  @return IxElementIterator
     */
    function all ($list = null)
    {
        $items = array();
        if (!($list instanceof IxElementIterator)) {
            $list = new CxElementList;
        }
        $list->begin($this->progenitor()->its, array());
        foreach ($this as $item) {
        	$items[] = $item;
        }
        $list->end($items);
        return $list;
    }

    /**
     *  Applies $callback to any given $mixed.
     *      - $this->$callback($mixed[0], ...), ... if $callback is string;
     *      - $callback($mixed[0], ...), ... if $callback is array callback.
     * 
     *  @param $callback string/array
     *  @param $mixed Data; see CxAbstractElementList::input()
     *  @param ...
     *  @return IxElementIterator
     */
    function apply ($callback, $mixed)
    {
        $args = func_get_args();
        $args = array_slice($args, 2);
        if (is_string($callback)) $callback = array($this, $callback);
        //  $output is left uninitialized intentionally
        $input = $mixed instanceof IxElementIterator ? $mixed : new CxElementList;
        $input->begin($this, $mixed);
        foreach ($input as $temp) {
            foreach (is_array($temp) ? $temp : array($temp) as $item) {
                $item = $input->process(call_user_func_array($callback,
                        array_merge(array($item), $args)));
                if (isset($item)) $output[] = $item;
            }
        }
        $input->end($output);
        return $input;
    }

    /**
     *  Inserts all the $mixed before $before or just prepends to the children by default.
     *  @param $mixed Data, see apply()
     *  @param $before any = null, see apply()
     *  @return IxElementIterator
     */
    function insert ($mixed, $before = null)
    {
        return $this->apply('insert_child', $mixed,
            $before ? $before : $this->dom_node()->firstChild);
    }

    protected function insert_child ($item, $before)
    {
        $node = $this->dom_node()->insertBefore($this->dom_node_of($item),
            $before ? $this->dom_node_of($before) : null);
        return $this->create_from($node);
    }

    /**
     *  Appends all the $mixed to the element.
     *  @param $mixed Data, see apply()
     *  @return IxElementIterator
     */
    function append ($mixed)
    {
        return $this->apply('append_child', $mixed);
    }

    protected function append_child ($item)
    {
        $node = $this->dom_node()->appendChild($this->dom_node_of($item));
        return $this->create_from($node);
    }

    /**
     *  Replace the element with the $new in the children of its parent.
     * 
     *  @param $new Node
     *  @return IxElementIterator; see CxElement::apply()
     */
    function replace ($new)
    {
        $new  = $this->dom_node_of($new);
        $node = $this->dom_node();
        $node = $node->parentNode->replaceChild($new, $node);
        return $this->create_from($new);
    }

    /**
     *  Removes the element from the children of its parent.
     *  @return IxElementIterator
     */
    function remove ()
    {
        $node = $this->dom_node();
        $parent = $node->parentNode;
        $parent->removeChild($node);
        return $this->create_from($parent);
    }

    /**
     *  Removes all the children.
     *  @return CxElement
     */
    function remove_all_children ()
    {
        $node = $this->dom_node();
        while ($child = $node->firstChild) {
            $node->removeChild($child);
        }
        return $this;
    }

    /**
     *  Returns a list of elements matching xpath $query.
     *  @param $query string xpath
     *  @return IxElementIterator
     */
    function select ($query)
    {
        $list = new CxElementList;
        $list->begin($this, $query);
        return $list;
    }

    /**
     *  Returns the value of attribute $name.
     *  @param $name string
     *  @return string : (string)element[$name]
     */
    function attribute ($name)
    {
        return (string)$this[$name];
    }

    /**
     *  Sets elements's attribute $name to $value; chainable.
     *  @param $name string
     *  @param $value any
     *  @return CxElement
     */
    function set_attribute ($name, $value)
    {
        $this[$name] = $value;
        return $this;
    }

    /**
     *  Returns the element's string value (child text node).
     *  @todo ??? parameter for typecasting.
     *  @return string
     */
    function value ()
    {
        return (string)$this;
    }

    /**
     *  Sets element's value; chainable.
     *  @param $value any
     *  @return CxElement
     */
    function set_value ($value)
    {
        $this[0] = $value;
        return $this;
    }

    /**
     *  Returns outer source of element.
     *  @return string
     */
    function source ()
    {
        return $this->asXML();
    }

    /**
     *  Sets outer source of element; chainable.
     *  It actually replace the element with document fragment, 
     *  so returned CxElement represents the elemen't parent.
     *  @param $source string
     *  @return CxElement
     */
    function set_source ($source)
    {
        $node = $this->dom_node();
        $parent = $node->parentNode;
        $f = $node->ownerDocument->createDocumentFragment();
        $f->appendXML($source);
        $parent->replaceChild($f, $node);
        return $this->create_from($parent);
    }

    /**
     *  Returns the element's inner source string.
     *  @return string
     */
    function inner_source ()
    {
        $result = '';
        foreach ($this->children() as $key=>$value) {
        	$result .= $value->source();
        }
        return $result;
    }

    /**
     *  Sets element's inner source.
     *  It replaces all the descendants.
     *  @param $source string
     *  @return CxElement
     */
    function set_inner_source ($source)
    {
        $this->remove_all_children();
        if (!$source) return;
        $node = $this->dom_node();
        $f = $node->ownerDocument->createDocumentFragment();
        $f->appendXML($source);
        $node->appendChild($f);
        return $this;
    }

}

//+----------------------------------------------------------------------------+
//+    IxElementIterator
//+----------------------------------------------------------------------------+

/**
 *  To implement external sources not derived from CxAbstractElementList.
 */
interface IxElementIterator extends Iterator, Countable
{
    /**
     *  Charges the element->items array and starts processing.
     *  @param $element IxElement
     *  @param $mixed Data
     *  @return void
     *  @see input
     */
    function begin (IxElement $element, $mixed);
    /**
     *  Post-processing routine (the pre-processing routine is current()).
     *  If it returns not null, the value will be put into the element->items array.
     *  @param $item any
     *  @return any
     */
    function process ($item);
    /**
     *  Tells that the processing is over.
     *  @param $output array keeps processed elements
     *  @return void
     */
    function end ($output);
}

//+----------------------------------------------------------------------------+
//+    CxAbstractElementList
//+----------------------------------------------------------------------------+

/**
 *  Represents a set/list of elements.
 *  @todo array access to attributes
 */
abstract class CxAbstractElementList implements IxElementIterator, IxElement
{
    /**
     *  $items relates to CxElement
     */
    public $items;  //  relates to CxElement 

    /**
     *  $base relates to CxElement.
     *  It keeps the first \b destination element; use CxAbstractElementList::reset() to reuse the object.
     */
    protected $base;  //  relates to CxElement 

    /**
     *  Convert $mixed into the array of CxElements.
     *  @param $base SimpleXMLElement
     *  @param $mixed Data
     *  @return array
     *  @todo optimize (30)
     */
    protected static function input (SimpleXMLElement $base, $mixed)
    {
        if (!isset($mixed))
            return array();
        if (is_array($mixed))
            return $mixed;
        if ($mixed instanceof IxElementIterator)
            return $mixed;
        if ($mixed instanceof CxElement)
            return array($mixed);
        if ($mixed instanceof SimpleXMLElement)
            return array($base->create_from(CxElement::dom_node_of($mixed)));
        if ($mixed instanceof DOMElement)
            return array($base->create_from($mixed));
        if (is_string($mixed)) {
            $array = array();
            foreach ($base->xpath($mixed) as $element)
                $array[] = $base->create_from(CxElement::dom_node_of($element));
            return $array;
        }
        throw new ExNotApplicable(
            __method__, $mixed
        );
    }

    /**
     *  Charges the element->items array and starts processing.
     *  CxAbstractElementList::reset() allows to reuse the object after it.
     *  @param $base IxElement
     *  @param $mixed Data
     *  @return void
     *  @see input
     */
    function begin (IxElement $base, $mixed)
    {
        if (!$this->base) {
            $this->items = $this->input($this->base = $base, $mixed);
        }
    }

    /**
     *  Tells that the processing is over.
     *  CxAbstractElementList::end() saves output into $items;
     *  @param $output array keeps processed elements
     *  @return void
     */
    function end ($output)
    {
        $this->items = (array)$output; /// @todo check???
    }

    /**
     *  Resets object's $base to reuse it.
     */
    function reset ()
    {
        unset($this->base);
    }

    /**
     *  Assigns element's $this to $var; chainable.
     *  @param $var IxElementIterator
     *  @return IxElementIterator
     */
    function into (& $var)
    {
        return $var = $this;
    }

    /**
     *  Applies $callback to all items.
     *      - $item->$callback(...), ... if $callback is string;
     *      - $callback(...), ... if $callback is array callback.
     * 
     *  @param $callback string/array
     *  @param ...
     *  @return IxElementIterator
     */
    function apply ($callback)
    {
        $args = func_get_args();
        $this->apply_to_array($output, $callback, array_slice($args, 1));
        $this->end($output);
        return $this;
    }

    /**
     *  @param $output array
     *  @param $callback 
     *  @param $args array = array()
     *  @return void
     *  @todo optimize (20)
     */
    protected function apply_to_array (& $output, $callback, $args = array())
    {
        foreach ($this as $temp) {
            foreach (is_array($temp) ? $temp : array($temp) as $item) {
                $this->apply_once($output, $callback, $item, $args);
            }
        }
    }

    /**
     *  @param $output array
     *  @param $callback 
     *  @param $item 
     *  @param $args array = array()
     *  @return void
     *  @todo optimixe (50)
     */
    protected function apply_once (& $output, $callback, $item, $args = array())
    {
        if (is_string($callback)) {
            $callback = array($item, $callback);
            if (!is_callable($callback)) return;
        }
        else {
            if (!is_callable($callback, 0, $method)) throw
                new ExNotImplemented($method);
            $args = array_merge(array($item), $args);
        }
        $result = $this->process(call_user_func_array($callback, $args));
        if (isset($result)) $output[] = $result;
        
    }

    /**
     *  Removes all the items represented by the object.
     */
    function remove ()
    {
        $this->apply_to_array($output, 'remove');
        return $this->base;
    }

    /**
     *  Replaces  all the items represented by the object.
     *  @param $element IxElement
     *  @return IxElementIterator
     */
    function replace (IxElement $element)
    {
        $result = array();
        foreach ($this as $item) {
            $result[] = $item->replace($element->duplicate());
        }
        $this->end($result);
        return $this;
    }

    /**
     *  Returns consolidated source for all the items.
     */
    function source ()
    {
        $this->apply_to_array($output, 'source');
        return join((array)$output);
    }

    /**
     *  Returns consolidated source for all the items.
     */
    function inner_source ()
    {
        return $this->source();
    }

    /**
     *  Delegates the call to all object's items.
     */
    function __call ($name, $args)
    {
        $this->apply_to_array($output, $name, $args);
        $this->end($output);
        return $this;
    }

    /**
     *  Returns consolidated text representation for all the items.
     *  @see CxElement::value().
     */
    function __toString ()
    {
        try {
            $this->apply_to_array($output, 'value');
            return join((array)$output);
        }
        catch (Exception $e) {
            return (string)$e;
        }
    }

}

//+----------------------------------------------------------------------------+
//+    CxElementListIterator
//+----------------------------------------------------------------------------+

abstract class CxElementListIterator extends CxAbstractElementList
{
    function key ()
    {
        return key($this->items);
    }

    function next ()
    {
        next($this->items);
    }

    function rewind ()
    {
        reset($this->items);
    }

    /**
     *  @done optimize (100)
     */
    function valid ()
    {
        return key($this->items) !== null;
    }

    function count ()
    {
        return count($this->items);
    }

}

//+----------------------------------------------------------------------------+
//+    CxCustomElementList
//+----------------------------------------------------------------------------+

/**
 *  Derive custom base/items element lists from this class.
 */
abstract class CxCustomElementList extends CxElementListIterator
{
    protected $mixed;  //  Data

    /**
     *  Constructs the object.
     *  @param $mixed Data see CxElement::apply()
     */
    function __construct ($mixed)
    {
        $this->mixed = $mixed;
    }

    function begin (IxElement $element, $mixed)
    {
        parent::begin($element, $this->mixed);
    }

}

//+----------------------------------------------------------------------------+
//+    CxElementList
//+----------------------------------------------------------------------------+

/**
 *  A result class of all functions, which return IxElementIterator
 *  if nothing is specified explicitely.
 */
class CxElementList extends CxElementListIterator
{
    /**
     *  The pre-processing point.
     *  @returns element item.
     *  @retval CxElement
     *  @retval array(CxElement)
     */
    function current ()
    {
        return current($this->items);
    }

    /**
     *  @param $item any
     *  @return any
     */
    function process ($item)
    {
        return $item;
    }

}

//+----------------------------------------------------------------------------+
//+    CxApply
//+----------------------------------------------------------------------------+

/**
 *  A helper class that makes it possible applying callback to mixed data.
 *  For example:
 *  @code
 *  $destination->append(new CxApply($items, 'duplicate'))
 *  @endcode
 *  duplicates all specified $items before appending them to the $destination.
 */
class CxApply extends CxCustomElementList
{
    private $callback;  //  

    private $args;  //  

    /**
     *  Constructs the object.
     *  @param $mixed Data
     *  @param $callback  = null
     *  @see CxElement::apply()
     */
    function __construct ($mixed, $callback = null)
    {
        parent::__construct($mixed);
        $this->callback = $callback;
        $args = func_get_args();
        $this->args = array_slice($args, 2);
    }

    /**
     *  The pre-processing point.
     *  @returns element item.
     *  @retval CxElement
     *  @retval array(CxElement)
     */
    function current ()
    {
        $item = current($this->items);
        if (!isset($this->callback)) return $item;
        $this->apply_once($output, $this->callback, $item, $this->args);
        return reset($output);
    }

    /**
     *  Main processing routine.
     *  If it returns not null, the value will be put into the element->items array.
     *  @param $item any
     *  @return any
     */
    function process ($item)
    {
        return $item;
    }

}
/**
 */

